package com.idealighter.game.games.doudizhu.manager;

import static com.idealighter.game.games.doudizhu.struct.CallType.CALL_LANDLORD;
import static com.idealighter.game.games.doudizhu.struct.CallType.GRAB_LANDLORD;
import static com.idealighter.game.games.doudizhu.struct.CallType.NOT_CALL_LANDLORD;
import static com.idealighter.game.games.doudizhu.struct.CallType.NOT_GRAB_LANDLORD;
import static com.idealighter.game.games.doudizhu.struct.CallType.getCallType;
import static com.idealighter.game.games.doudizhu.struct.DouDiZhuTable.SEAT_NUM;

import com.alibaba.fastjson.JSON;

import com.google.common.collect.TreeMultimap;
import com.google.common.eventbus.Subscribe;
import com.google.common.math.IntMath;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;

import com.idealighter.game.backpack.manager.BackpackMgr;
import com.idealighter.game.common.CommonMsgMgr;
import com.idealighter.game.core.common.Game;
import com.idealighter.game.core.constant.Operator;
import com.idealighter.game.core.error.ErrorCode;
import com.idealighter.game.core.util.NameMaker;
import com.idealighter.game.dblog.service.DbLogService;
import com.idealighter.game.dictionary.dic.DoudizhuMatchRoomDic;
import com.idealighter.game.dictionary.dic.DoudizhuTimeDic;
import com.idealighter.game.dictionary.dic.PlayerHeadSrcDic;
import com.idealighter.game.dictionary.domain.DoudizhuMatchRoomDomain;
import com.idealighter.game.dictionary.domwrapper.DoudizhuMatchRoomDomWrapper;
import com.idealighter.game.dictionary.domwrapper.DoudizhuMatchRoomDomWrapper.Reward;
import com.idealighter.game.event.manager.EventMgr;
import com.idealighter.game.event.struct.PlayerExitEvent;
import com.idealighter.game.event.struct.ShutdownRoomEvent;
import com.idealighter.game.event.struct.StartRoomEvent;
import com.idealighter.game.gamehall.event.EnterRoomEvent;
import com.idealighter.game.gamehall.event.ExitRoomEvent;
import com.idealighter.game.games.common.AbstractRoom;
import com.idealighter.game.games.doudizhu.job.DouDiZhuMatchJob;
import com.idealighter.game.games.doudizhu.struct.CallType;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuAiSeat;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuCard;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuCardsType;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuMatch;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuRoom;
import com.idealighter.game.games.doudizhu.struct.DouDiZhuTable;
import com.idealighter.game.games.doudizhu.util.CardsTypeComparator;
import com.idealighter.game.games.doudizhu.util.CardsTypeFinder;
import com.idealighter.game.games.doudizhu.util.CardsTypeGetter;
import com.idealighter.game.log.core.LogReason;
import com.idealighter.game.log.struct.game.DouDiZhuMatchLog;
import com.idealighter.game.login.manager.LoginMgr;
import com.idealighter.game.player.manager.PlayerMgr;
import com.idealighter.game.player.struct.Player;
import com.idealighter.game.schedule.manager.ScheduleMgr;
import com.idealighter.game.server.ExecutorMgr;
import com.idealighter.game.server.core.executor.DisruptorExecutor;
import com.idealighter.utils.code.RandCodeUtil;

import java.math.RoundingMode;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 斗地主。 .
 * 
 * @date 2016年2月25日 上午9:45:24
 *
 */
@Singleton
public class DouDiZhuMgr {

  private static final Logger LOG = LoggerFactory.getLogger(DouDiZhuMgr.class);

  // 快速比赛
  public static final int FAST_MATCH = 1;
  // 定时比赛
  public static final int TIMER_MATCH = 2;
  // 玩家动作动作延时(有网络延时等原因，要比客户端的定时长一点)
  public static final int PLAYER_ACTION_DELAY = 2;
  // 系统托管动作时间(秒)
  public static final int HOST_ACTION_SECONDS = 1;
  // 机器人动作最大时间(秒)
  public static final int ROBOT_ACTION_MAX_SECONDS = 5;
  // 农民的牌数量
  public static final int FARMER_CARDS = 17;
  // 底牌数量
  public static final int HIDDEN_CARDS = 3;
  // 通知比赛时间(秒)
  public static final int NOTICE_MATCH_TIME = 60;

  private final DouDiZhuDataMgr dataMgr;
  private final DouDiZhuMsgMgr msgMgr;
  private final PlayerMgr playerMgr;
  private final CommonMsgMgr commonMsgMgr;
  private DoudizhuTimeDic timeDic;
  private final EventMgr eventMgr;
  private final ScheduleMgr scheduleMgr;
  // 游戏线程executor,可以执行普通任务和定时任务
  public final DisruptorExecutor gameExecutor;
  private final DoudizhuMatchRoomDic matchRoomDic;
  private final PlayerHeadSrcDic playerIconDic;
  private final BackpackMgr backpackMgr;
  // 不需要斗地主机器人
  private final boolean notDdzRobot;

  @Inject
  private LoginMgr loginMgr;

  /**
   * 构造函数.
   * 
   * @param dataMgr .
   * @param msgMgr .
   * @param playerMgr .
   * @param commonMsgMgr .
   * @param timeDic .
   * @param eventMgr .
   * @param scheduleMgr .
   * @param executorMgr .
   * @param matchRoomDic .
   * @param playerIconDic .
   * @param backpackMgr .
   * @param notDdzRobot .
   */
  @Inject
  public DouDiZhuMgr(DouDiZhuDataMgr dataMgr, DouDiZhuMsgMgr msgMgr, PlayerMgr playerMgr,
      CommonMsgMgr commonMsgMgr, DoudizhuTimeDic timeDic, EventMgr eventMgr,
      ScheduleMgr scheduleMgr, ExecutorMgr executorMgr, DoudizhuMatchRoomDic matchRoomDic,
      PlayerHeadSrcDic playerIconDic, BackpackMgr backpackMgr,
      @Named("notDdzRobot") boolean notDdzRobot) {
    this.dataMgr = dataMgr;
    this.msgMgr = msgMgr;
    this.playerMgr = playerMgr;
    this.commonMsgMgr = commonMsgMgr;
    this.timeDic = timeDic;
    this.eventMgr = eventMgr;
    this.scheduleMgr = scheduleMgr;
    this.matchRoomDic = matchRoomDic;
    this.playerIconDic = playerIconDic;
    this.backpackMgr = backpackMgr;
    this.gameExecutor = executorMgr.getGameExecutor(Game.DOU_DI_ZHU.getModuleId());
    this.notDdzRobot = notDdzRobot;

    eventMgr.register(this);
    // 调度比赛
    scheduleRooms();
  }

  /**
   * T玩家离开事件监听.
   * 
   * @param event 玩家离开事件.
   */
  @Subscribe
  public void onPlayerExitGame(PlayerExitEvent event) {
    gameExecutor.execute(() -> {
      // 退赛
      withdraw(event.player);
    });
  }

  /**
   * 开启房间事件监听.
   * 
   * @param event 开启房间.
   */
  @Subscribe
  public void onStartRoom(StartRoomEvent event) {
    gameExecutor.execute(() -> {
      if (event.getGame() == Game.DOU_DI_ZHU) {
        matchRoomDic.load();
      }
    });
  }

  /**
   * 关闭房间监听事件.
   * 
   * @param event 关闭房间事件.
   */
  @Subscribe
  public void onShutDownRoom(ShutdownRoomEvent event) {
    gameExecutor.execute(() -> {
      if (event.getGame() == Game.DOU_DI_ZHU) {
        matchRoomDic.load();
        AbstractRoom room = dataMgr.getRoom(event.getRoomId());
        for (Long playerId : room.getPlayers()) {
          Player player = playerMgr.getPlayer(playerId);
          if (player != null) {
            loginMgr.noticeLogoutForGameExit(player);
          }
        }
      }
    });
  }

  /**
   * schedule房间 .
   */
  private void scheduleRooms() {
    for (DouDiZhuRoom room : dataMgr.getRooms().values()) {
      scheduleRoom(room);
    }
  }

  /**
   * 调度游戏定时房间比赛 .
   * 
   * @param room .
   */
  public void scheduleRoom(DouDiZhuRoom room) {
    DoudizhuMatchRoomDomain matchRoomDom = matchRoomDic.get(room.getId().intValue());
    if (matchRoomDom.getType() == TIMER_MATCH) {
      /*
       * 定时比赛schedule .
       */
      JobDataMap jobData = new JobDataMap();
      jobData.put("room", dataMgr.getRoom(matchRoomDom.getId()));
      CronTrigger trigger = scheduleMgr.scheduleJob(DouDiZhuMatchJob.class,
          "DouDiZhuMath-" + matchRoomDom.getId(), matchRoomDom.getCron(), jobData);
      scheduleNextTimerMatch(room, trigger.getNextFireTime().getTime());
    } else if (matchRoomDom.getType() == FAST_MATCH) {
      /*
       * 快速比赛schedule .
       */
      scheduleNextFastMatch(room);
      if (notDdzRobot) {
        scheduleMgr.scheduleAtFixedRate(() -> {
          DouDiZhuMatch match = room.curMatch;
          if (match.applyNum < match.matchNum) {
            match.applyNum++;
            msgMgr.sendMatchInfoUpdateMsg(match);
          } else if (match.applyNum == match.matchNum) { // 快速赛开始
            startMatch(match);
            scheduleNextFastMatch(room);
          }
          // TODO:测试15秒一个
        }, 5, 15, TimeUnit.SECONDS, gameExecutor);
      }
    }
  }

  /**
   * schedule定时房间下一场快速赛 .
   * 
   * @param room .
   */
  public void scheduleNextFastMatch(DouDiZhuRoom room) {
    DoudizhuMatchRoomDomain matchRoomDom = matchRoomDic.get(room.getId().intValue());
    int matchNum = matchRoomDom.getNum();
    // 快速的比赛时间是大概的,平均一个玩家3秒
    long matchTime = System.currentTimeMillis() + matchNum * 3 * 1000;
    DouDiZhuMatch match = new DouDiZhuMatch(room, matchNum, matchTime, FAST_MATCH);
    room.curMatch = match;
  }

  /**
   * schedule定时房间下一场比赛 .
   * 
   * @param room .
   * @param matchTime .
   */
  public void scheduleNextTimerMatch(DouDiZhuRoom room, long matchTime) {
    DoudizhuMatchRoomDomain matchRoomDom = matchRoomDic.get(room.getId().intValue());
    // 比赛通知delay
    long matchNoticeDelay = matchTime - System.currentTimeMillis() - NOTICE_MATCH_TIME * 1000;
    DouDiZhuMatch match = new DouDiZhuMatch(room, matchRoomDom.getNum(), matchTime, TIMER_MATCH);
    room.curMatch = match;
    // 提前一点时间通知比赛
    scheduleMgr.schedule(() -> noticeMatch(match), matchNoticeDelay, TimeUnit.MILLISECONDS,
        gameExecutor);
  }

  /**
   * 通知报名玩家比赛 .
   * 
   * @param match .
   */
  public void noticeMatch(DouDiZhuMatch match) {
    long now = System.currentTimeMillis();
    for (long playerId : match.appliedPlayers) {
      Player player = playerMgr.getPlayer(playerId);
      if (player != null) {
        commonMsgMgr.sendMatchNoticeDlgMsg(player,
            Game.DOU_DI_ZHU.getDesc() + "-" + match.room.getName(),
            (int) ((match.matchTime - now) / 1000));
      }
    }
  }

  /**
   * 进入游戏大厅 .
   * 
   * @param player 玩家.
   */
  public void enterGameHall(Player player) {
    if (player.curRoom != null) {
      Game game = player.curRoom.game();
      LOG.warn("[斗地主]玩家[{}][{}]在[{}]游戏中，暂时无法进入游戏", player.getId(), player.getPlayerName(),
          game.getDesc());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ALREADY_IN, Operator.SYSTEM,
          game.getDesc());
      return;
    }

    msgMgr.sendEnterGameHallMsg(player);
    LOG.info("[斗地主]玩家[{}][{}]进入大厅成功", player.getPlayerName(), player.getId());
  }

  /**
   * 玩家进入房间 .
   * 
   * @param player 玩家.
   * @param roomId .
   */
  public void enterRoom(Player player, int roomId) {
    AbstractRoom curRoom = player.curRoom;
    if (curRoom != null) {
      Game game = curRoom.game();
      LOG.warn("[斗地主]玩家[{}][{}]当前在游戏[{}]房间[{}]中不能进入房间", player.getId(), player.getPlayerName(),
          curRoom.getId());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ALREADY_IN, Operator.SYSTEM,
          game.getDesc());
      return;
    }

    DouDiZhuRoom room = dataMgr.getRoom(roomId);
    if (room == null) {
      LOG.error("[斗地主]玩家[{}][{}]进入的房间[{}]不存在", player.getPlayerName(), player.getId(), roomId);
      return;
    }

    if (matchRoomDic.get(roomId).getIsActive() != 1) {
      LOG.error("[斗地主]玩家[{}][{}]进入的房间[{}]已经关闭", player.getPlayerName(), player.getId(), roomId);
      return;
    }

    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat != null) {
      DouDiZhuRoom matchRoom = seat.table.match.room;
      LOG.error("[斗地主]玩家[{}][{}]正在房间[{}][{}]进行比赛，进入的房间[{}][{}]失败", player.getPlayerName(),
          player.getId(), matchRoom.getId(), matchRoom.getName(), room.getId(), room.getName());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ALREADY_IN_MATCH, Operator.SYSTEM);
      return;
    }

    // 修改玩家当前房间
    player.curRoom = room;
    room.getPlayers().add(player.getId());
    // 发送进入房间消息
    msgMgr.sendEnterRoomMsg(player, room);
    LOG.info("[斗地主]玩家[{}][{}]进入房间[{}]成功", player.getPlayerName(), player.getId(), roomId);

    // 触发进入房间事件
    eventMgr.post(new EnterRoomEvent(player, room));
  }

  /**
   * 玩家报名 .
   * 
   * @param player 玩家.
   */
  public void apply(Player player) {
    AbstractRoom curRoom = player.curRoom;
    if (!(curRoom instanceof DouDiZhuRoom)) {
      LOG.warn("[斗地主]玩家[{}][{}]当前不在房间中不能报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      return;
    }

    DouDiZhuRoom room = (DouDiZhuRoom) curRoom;
    DouDiZhuMatch match = room.curMatch;
    Set<Long> appliedPlayers = match.appliedPlayers;

    if (appliedPlayers.contains(player.getId())) {
      LOG.warn("[斗地主]玩家[{}][{}]已经报过比赛名不能重复报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_NOT_MATCH_REQUIREMENT, Operator.SYSTEM);
      return;
    }

    if (match.applyNum >= match.matchNum) {
      LOG.warn("[斗地主]玩家[{}][{}]已经超过报名人数上限不能报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_START_MSG, Operator.SYSTEM);
      return;
    }

    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat != null) {
      LOG.warn("[斗地主]玩家[{}][{}]已经在比赛中不能报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_NOT_MATCH_REQUIREMENT, Operator.SYSTEM);
      return;
    }

    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper = matchRoomDic.get(room.getId().intValue());
    // 参加比赛需要的卷
    int tickets = matchRoomDomWrapper.getTickets();
    if (player.items().getOrDefault(tickets, 0) > 0) {
      backpackMgr.removeItem(player, tickets, 1, LogReason.DOUDIZHU_APPLY);
    } else if (player.getGold() >= matchRoomDomWrapper.getGold()) {
      playerMgr.minusGold(player, matchRoomDomWrapper.getGold(), LogReason.DOUDIZHU_APPLY);
    } else {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_NOT_MATCH_REQUIREMENT, Operator.SYSTEM);
      return;
    }

    appliedPlayers.add(player.getId());
    DoudizhuMatchRoomDomain matchRoomDom = matchRoomDic.get(room.getId().intValue());
    match.applyNum++;
    if (match.applyNum == match.matchNum && matchRoomDom.getType() == FAST_MATCH) { // 开始快速赛
      startMatch(match);
      scheduleNextFastMatch(room);
    }

    msgMgr.sendApplyMsg(player, 0);
    msgMgr.sendMatchInfoUpdateMsg(room.curMatch);
    LOG.info("[斗地主]玩家[{}][{}]报名房间[{}][{}]成功", player.getId(), player.getPlayerName(),
        curRoom.getId(), curRoom.getName());
  }

  /**
   * 取消报名 .
   * 
   * @param player 玩家.
   */
  public void cancelApply(Player player) {
    AbstractRoom curRoom = player.curRoom;
    if (!(curRoom instanceof DouDiZhuRoom)) {
      LOG.warn("[斗地主]玩家[{}][{}]当前不在房间中不能取消报名", player.getId(), player.getPlayerName());
      return;
    }

    DouDiZhuRoom room = (DouDiZhuRoom) curRoom;
    DouDiZhuMatch match = room.curMatch;

    if (!match.appliedPlayers.remove(player.getId())) {
      LOG.warn("[斗地主]玩家[{}][{}]没有报名不能取消报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      return;
    }

    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat != null) {
      LOG.warn("[斗地主]玩家[{}][{}]已经在比赛中不能取消报名", player.getId(), player.getPlayerName(),
          curRoom.getId());
      return;
    }
    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper = matchRoomDic.get(room.getId().intValue());
    playerMgr.addGold(player, matchRoomDomWrapper.getGold(), LogReason.DOUDIZHU_CANCEL);

    match.applyNum--;
    msgMgr.sendMatchInfoUpdateMsg(match);
    msgMgr.sendCancelApplyMsg(player);
    LOG.info("[斗地主]玩家[{}][{}]取消报名房间[{}][{}]成功", player.getId(), player.getPlayerName(),
        curRoom.getId(), curRoom.getName());
  }

  /**
   * 开始斗地主比赛 .
   * 
   * @param match .
   */
  public void startMatch(DouDiZhuMatch match) {
    // 真实的报名比赛的玩家
    List<Long> appliedPlayers = new ArrayList<>(match.appliedPlayers);
    DouDiZhuRoom room = match.room;
    // 实际参与比赛的人(玩家在线，且当前进入比赛房间的人)
    List<Player> matchPlayers = new ArrayList<>();
    match.matchTime = System.currentTimeMillis();
    for (Long applicant : appliedPlayers) {
      Player player = playerMgr.getPlayer(applicant);
      if (room.getPlayers().contains(applicant) && player != null) {
        matchPlayers.add(player);
      }
    }

    if (matchPlayers.size() == 0) {
      LOG.info("[斗地主]房间[{}][{}]比赛[{}][{}]没有真实玩家参赛不开赛", room.getName(), room.getId(), match.id,
          DateFormat.getDateTimeInstance().format(match.matchTime));
      return;
    }

    /*
     * 创建比赛的Seat(真实玩家和陪玩机器人) .
     */
    // 比赛人数
    int matchNum = match.matchNum;
    // 玩家索引
    int playerIndex = 0;
    // 机器人玩家id种子
    long robotPlayerIdSeed = System.currentTimeMillis();
    // 玩家报名顺序种子
    int applyOrderSeed = RandCodeUtil.random(matchNum);
    List<DouDiZhuAiSeat> matchSeats = match.seats;
    for (int i = 0; i < matchNum; i++) {
      DouDiZhuAiSeat seat = null;

      if (playerIndex < matchPlayers.size()) { // 创建真实seat
        Player player = matchPlayers.get(playerIndex++);
        seat = DouDiZhuAiSeat.createPlayerSeat(player, (applyOrderSeed++) % matchNum);
        dataMgr.updatePlayerSeats(seat.getPlayerId(), seat);
        doEnterTable(seat);
      } else { // 创建假seat
        String robotName = NameMaker.generateName();
        int robotIcon = RandCodeUtil.randomList(playerIconDic.getIcons());
        seat = DouDiZhuAiSeat.createRobotSeat(robotPlayerIdSeed++, robotName, robotIcon,
            (applyOrderSeed++) % matchNum);
      }

      matchSeats.add(seat);

      /*
       * TODO:如果玩家在指定时间内 . 没有完成比赛 清除玩家信息(有些客户出现斗地主卡号的情况)
       */
      final DouDiZhuAiSeat seatTmp = seat;
      scheduleMgr.schedule(() -> {
        DouDiZhuTable table = seatTmp.table;
        if (table != null && table.match != room.curMatch
            && seatTmp == dataMgr.getPlayerSeat(seatTmp.getPlayerId())) {
          dataMgr.removePlayerSeat(seatTmp.getPlayerId());
        }
      }, 60, TimeUnit.MINUTES, gameExecutor);
    }

    // 排序
    sortMatchSeats(matchSeats);
    // 开始预赛 (即0轮)
    startMatchRuns(match, 0);
  }

  /**
   * 进入桌子 .
   * 
   * @param seat 座位信息.
   */
  private void doEnterTable(DouDiZhuAiSeat seat) {
    msgMgr.sendEnterTableMsg(seat);
    LOG.info("[斗地主]玩家[{}][{}]进入桌子成功", seat.getPlayerName(), seat.getPlayerId());
  }

  /**
   * 准备 .
   * 
   * @param player 玩家.
   */
  public void ready(Player player) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null || !seat.table.match.seats.contains(seat)) {
      return;
    }

    doReady(seat);
  }

  /**
   * schedule桌子准备 .
   * 
   * @param table .
   */
  public void scheduleReady(DouDiZhuTable table) {
    // 准备时间
    int readyTime = timeDic.get(1).getTime();
    for (DouDiZhuAiSeat seat : table.seats) {
      if (seat.isRobot()) {
        seat.ready = true;
      } else {
        seat.stepFuture = scheduleMgr.schedule(() -> doReady(seat),
            seat.sysHost ? HOST_ACTION_SECONDS : readyTime, TimeUnit.SECONDS, gameExecutor);
      }
    }
  }

  /**
   * 准备 .
   * 
   * @param seat 座位信息.
   */
  private void doReady(DouDiZhuAiSeat seat) {
    seat.stepFuture.cancel(false);
    seat.ready = true;
    DouDiZhuTable table = seat.table;
    if (table != null && table.readyOver() && table.nextCallOrder == -1) { // 桌子准备好且没有开始游戏
      startGame(table);
    }

    LOG.info("[斗地主]玩家[{}][{}]准备", seat.getPlayerId(), seat.getPlayerName());
  }

  /**
   * 开始比赛轮数 .
   * 
   * @param match .
   * @param runs . 第几轮，0：预赛 .
   */
  private void startMatchRuns(DouDiZhuMatch match, int runs) {

    // 参与比赛的seat
    List<DouDiZhuAiSeat> matchSeats = match.seats;
    // 真实玩家seats
    List<DouDiZhuAiSeat> playerSeats = match.playerSeats();
    // 避免分桌时是乱序的
    Collections.shuffle(playerSeats);
    // 机器人seats
    List<DouDiZhuAiSeat> robotSeats = match.robotSeats();
    Collections.shuffle(robotSeats);
    match.runs = runs;

    DouDiZhuRoom room = match.room;

    if (playerSeats.size() == 0) {
      LOG.info("[斗地主]房间[{}][{}]比赛[{}][{}]第[{}]轮没有真实玩家参赛不开赛", room.getName(), room.getId(), match.id,
          DateFormat.getDateTimeInstance().format(match.matchTime), runs);
      return;
    }
    List<DouDiZhuTable> tables = match.tables;
    /*
     * 开始每轮比赛时都创建桌子(维护桌子和座位状态容易bug)，算法要保证每张桌子有至少能包含一个真实玩家 .
     */
    tables.clear();
    // 最大桌子数
    int maxTableNum = IntMath.divide(match.seats.size(), SEAT_NUM, RoundingMode.UP);
    for (int i = 0; i < playerSeats.size() && i < maxTableNum; i++) {
      tables.add(new DouDiZhuTable(i + 1, match));
    }

    /*
     * 决赛每轮玩家的积分要变,开方后*0.1 .
     */
    if (runs > 0) {
      for (DouDiZhuAiSeat seat : matchSeats) {
        if (seat.cedits < 0) {
          seat.cedits = (long) (-Math.sqrt(-seat.cedits) * 0.1);
        } else {
          seat.cedits = (long) (Math.sqrt(seat.cedits) * 0.1);
        }
      }
    }

    /*
     * 分配比赛的位置,已经保证每张桌子有至少有一个玩家,优先分配真玩家 . 算法：玩家index % 桌子数 = 桌子号，玩家index / 桌子数 % 3 = order
     */
    // 桌子数
    int tableNum = tables.size();
    for (int order = 0; order < SEAT_NUM; order++) {
      for (int tblIndex = 0; tblIndex < tableNum; tblIndex++) {
        DouDiZhuTable table = tables.get(tblIndex);
        int seatIndex = tblIndex + order * tableNum;
        DouDiZhuAiSeat seat = null;

        if (seatIndex < playerSeats.size()) {
          seat = playerSeats.get(seatIndex);
        } else {
          seat = robotSeats.get(seatIndex - playerSeats.size());
        }

        table.seats.add(seat);
        seat.table = table;
        seat.setOrder(order);
      }
    }

    for (DouDiZhuTable table : tables) {
      scheduleReady(table);
    }

    LOG.info("[斗地主]房间[{}][{}]比赛[{}]第[{}]轮开始", match.room.getId(), match.room.getName(), match.id,
        match.runs);
  }

  /**
   * 发牌,每人先发17张 .
   * 
   * @param table .
   */
  private void doDealCards(DouDiZhuTable table) {
    Collections.shuffle(table.cards);
    List<DouDiZhuCard> cards = table.cards;
    int cardIndex = 0;

    for (DouDiZhuAiSeat seat : table.seats) {
      seat.cards.clear();
      seat.cards.addAll(cards.subList(cardIndex, cardIndex += FARMER_CARDS));
      // ai需要计算牌型
      seat.calcCardsType();
    }

    // 随机叫牌的位置
    table.nextCallOrder = RandCodeUtil.random(table.seats.size());
    // 向同桌的玩家发送牌消息
    msgMgr.sendDealCardsMsg(table);

    // schedule玩家叫牌
    scheduleCallCard(table.seats.get(table.nextCallOrder), null);
  }

  /**
   * 玩家叫牌，“叫牌”分最高的玩家就是本局的“地主” .
   * 
   * @param player 玩家.
   * @param type . 0:不叫地主,1:叫地主,2:不抢地主,3:抢地主 .
   */
  public void callCard(Player player, int type) {
    // 玩家的叫牌类型
    CallType callType = getCallType(type);
    if (callType == null) {
      LOG.error("[斗地主]玩家[{}][{}]叫牌类型[{}]", player.getPlayerName(), player.getId(), type);
      return;
    }

    // 座位
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[斗地主]玩家[{}][{}]还未进入牌桌不能叫牌", player.getPlayerName(), player.getId());
      return;
    }

    // 牌桌
    DouDiZhuTable table = seat.table;
    if (table.nextCallOrder != seat.getOrder()) {
      LOG.error("[斗地主]当前叫牌顺序为[{}],玩家[{}][{}]顺序为[{}]不能叫牌", table.nextCallOrder,
          player.getPlayerName(), player.getId(), seat.getOrder());
      return;
    }

    switch (callType) {
      case NOT_CALL_LANDLORD: // 不叫，不叫以后不能抢地主
        notCallLandlord(seat);
        break;
      case CALL_LANDLORD: // 叫地主
        callLandlord(seat);
        break;
      case NOT_GRAB_LANDLORD: // 不抢地主
        notGrabLandLord(seat);
        break;
      case GRAB_LANDLORD: // 抢地主,不叫地主的玩家不能再抢地主
        grabLandlord(seat);
        break;
      default:
        break;
    }
  }

  /**
   * schedule玩家叫牌 .
   * 
   * @param seat 座位信息.
   * @param preCallType . 上一个叫牌类型 .
   */
  private void scheduleCallCard(DouDiZhuAiSeat seat, CallType preCallType) {
    int callTime = timeDic.get(2).getTime() + PLAYER_ACTION_DELAY;

    seat.stepFuture =
        scheduleMgr
            .schedule(() -> autoCallCard(seat, preCallType),
                seat.isRobot() ? RandCodeUtil.random(1, ROBOT_ACTION_MAX_SECONDS)
                    : seat.sysHost ? HOST_ACTION_SECONDS : callTime,
                TimeUnit.SECONDS, gameExecutor);
  }

  /**
   * 自动叫牌 .
   * 
   * @param seat 座位信息.
   * @param preCallType . 上一个叫牌类型 .
   */
  private void autoCallCard(DouDiZhuAiSeat seat, CallType preCallType) {
    if (preCallType == null || preCallType == NOT_CALL_LANDLORD) {
      if (seat.isRobot()) {
        if (seat.callLandlord()) {
          callLandlord(seat);
        } else {
          notCallLandlord(seat);
        }
      } else {
        notCallLandlord(seat);
      }
    } else {
      if (seat.isRobot()) {
        if (seat.grabLandlord()) {
          grabLandlord(seat);
        } else {
          notGrabLandLord(seat);
        }
      } else {
        notGrabLandLord(seat);
      }
    }
  }

  /**
   * 不叫地主 .
   * 
   * @param seat 座位信息.
   */
  private void notCallLandlord(DouDiZhuAiSeat seat) {
    if (seat.callType != null) {
      LOG.error("[斗地主]玩家[{}][{}]已经叫过[{}]不能'不叫地主'", seat.getPlayerName(), seat.getPlayerId(),
          seat.callType.desc);
      return;
    }
    seat.stepFuture.cancel(false);
    seat.callType = NOT_CALL_LANDLORD;
    seat.landlord = false;
    // 叫牌后处理,主要是判断是否叫牌结束
    afterCallCard(seat);
  }

  /**
   * 叫地主 . 叫地主： 游戏开始后，系统随机选择一位玩家开始叫地主。 每位玩家最多只有1次叫地主机会，三位玩家都没叫地主，游戏自动结束，重新洗牌、发牌。积分只扣除平台使用费。
   * 叫地主之后，游戏倍数不变。 . 地主被叫之后，叫地主流程结束。进入抢地主流程。 .
   * 
   * @param seat 座位信息.
   */
  private void callLandlord(DouDiZhuAiSeat seat) {
    if (seat.callType != null) {
      LOG.error("[斗地主]玩家[{}][{}]已经叫过[{}]不能'叫地主'", seat.getPlayerName(), seat.getPlayerId(),
          seat.callType.desc);
      return;
    }
    seat.stepFuture.cancel(false);
    seat.callType = CALL_LANDLORD;
    seat.landlord = true;
    // 叫牌后处理,主要是判断是否叫牌结束
    afterCallCard(seat);
  }

  /**
   * 不抢地主 .
   * 
   * @param seat 座位信息.
   */
  private void notGrabLandLord(DouDiZhuAiSeat seat) {
    if (seat.callType != NOT_GRAB_LANDLORD && seat.callType != GRAB_LANDLORD) {
      seat.stepFuture.cancel(false);
      seat.callType = NOT_GRAB_LANDLORD;
      seat.landlord = false;
    } else {
      LOG.error("[斗地主]玩家[{}][{}]已经叫过[{}]不能'不抢地主'", seat.getPlayerName(), seat.getPlayerId(),
          seat.callType);
      return;
    }
    // 叫牌后处理,主要是判断是否叫牌结束
    afterCallCard(seat);
  }

  /**
   * 抢地主 . 抢地主： 有玩家叫地主之后，进入抢地主流程。每位玩家有且只有1次抢地主机会。最后一位选择抢地主的玩家成为地主。当有玩家抢地主后，倍数*2。
   * eg：游戏初始倍数为1，有人叫地主后，第一位玩家抢地主，倍数变为2倍，第二位玩家抢地主，倍数变为4倍，第三位玩家抢地主，倍数变为8倍。 .
   * 
   * @param seat 座位信息.
   */
  private void grabLandlord(DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;
    if (seat.callType == null || seat.callType == CALL_LANDLORD) {
      seat.stepFuture.cancel(false);
      seat.callType = CallType.GRAB_LANDLORD;
      DouDiZhuAiSeat landlordSeat = table.landlord();
      landlordSeat.landlord = false;
      seat.landlord = true;
      // 倍数 * 2 .
      table.multiple <<= 1;
    } else {
      LOG.error("[斗地主]玩家[{}][{}]已经叫过[{}]不能‘抢地主’", seat.getPlayerName(), seat.getPlayerId(),
          seat.callType);
      return;
    }
    // 叫牌后处理,主要是判断是否叫牌结束
    afterCallCard(seat);
  }

  /**
   * 叫牌后处理,主要是判断是否叫牌结束 .
   * 
   * @param seat 座位信息.
   */
  private void afterCallCard(DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;

    // 所有人都没有叫地主，默认第一个人叫庄且为地主
    if (table.allNotCallLandlord()) {
      LOG.info("[斗地主]比赛[{}]桌子[{}]所有玩家都是不叫地主第一个位置的人做地主", table.match.id, table.getId());
      table.seats.get(0).landlord = true;
    }

    // 下一个叫牌的seat
    DouDiZhuAiSeat nextCallSeat = table.nextCallCardSeat(seat);
    // 是否叫牌结束
    boolean callOver = nextCallSeat == null;

    if (callOver) { // 叫牌结束,地主确定后，如果地主是托管直接出牌
      // 叫牌发送叫牌结果
      msgMgr.sendCallCardMsg(seat, callOver);
      callCardOver(seat.table);
    } else {
      // 修改当前叫分的位置
      table.nextCallOrder = nextCallSeat.getOrder();
      // 叫牌发送叫牌结果
      msgMgr.sendCallCardMsg(seat, callOver);
      // schedule玩家叫牌
      scheduleCallCard(nextCallSeat, seat.callType);
    }
  }

  /**
   * 叫牌结束 .
   * 
   * @param table .
   */
  private void callCardOver(DouDiZhuTable table) {
    DouDiZhuAiSeat landlord = table.landlord();
    table.nextPlayOrder = landlord.getOrder();
    table.prePlayOrder = landlord.getOrder();
    // 将底牌放入自己的牌中
    landlord.cards.addAll(table.hiddenCards());
    // 重新计算牌型
    landlord.calcCardsType();
    // 叫牌结束 发送底牌
    msgMgr.sendHiddenCardsMsg(table);

    // seat如果是托管的话加倍？
    for (DouDiZhuAiSeat seat : table.seats) {
      scheduleDouble(seat);
    }

    LOG.info("[斗地主]牌桌[{}]叫牌结束,玩家[{}][{}]是地主", table.getId(), landlord.getPlayerId(),
        landlord.getPlayerName());
  }

  /**
   * schedule玩家加倍 .
   * 
   * @param seat 座位信息.
   */
  private void scheduleDouble(DouDiZhuAiSeat seat) {
    if (seat.sysHost) { // 系统托管不加倍
      seat.stepFuture = scheduleMgr.schedule(() -> doDoubled(seat, false), HOST_ACTION_SECONDS,
          TimeUnit.SECONDS, gameExecutor);
    } else if (seat.isRobot()) { // 机器人加倍
      seat.stepFuture = scheduleMgr.schedule(() -> doDoubled(seat, seat.doDouble()),
          RandCodeUtil.random(1, ROBOT_ACTION_MAX_SECONDS), TimeUnit.SECONDS, gameExecutor);
    } else { // 普通玩家加倍
      int doubleTime = timeDic.get(3).getTime() + PLAYER_ACTION_DELAY;
      seat.stepFuture = scheduleMgr.schedule(() -> doDoubled(seat, false), doubleTime,
          TimeUnit.SECONDS, gameExecutor);
    }
  }

  /**
   * 玩家加倍 .
   * 
   * @param player 玩家.
   * @param doubled .
   */
  public void doubled(Player player, boolean doubled) {
    // 座位
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[斗地主]玩家[{}][{}]还未进入牌桌不能加倍", player.getPlayerName(), player.getId());
      return;
    }

    doDoubled(seat, doubled);
  }

  /**
   * 加倍 .
   * 
   * @param seat 座位信息.
   * @param doubled .
   */
  private void doDoubled(DouDiZhuAiSeat seat, boolean doubled) {
    seat.stepFuture.cancel(false);
    seat.doubled = doubled;
    msgMgr.sendDoubleMsg(seat, doubled);

    DouDiZhuTable table = seat.table;
    if (table.doubledOver()) {
      schedulePlayCards(table.landlord());
    }
  }

  /**
   * schedule玩家出牌 .
   * 
   * @param seat 座位信息.
   */
  private void schedulePlayCards(DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;
    if (table.nextPlayOrder != seat.getOrder()) {
      return;
    }
    // 真实玩家出牌时间限制
    int playTime = timeDic.get(4).getTime() + PLAYER_ACTION_DELAY;

    seat.stepFuture =
        scheduleMgr
            .schedule(() -> autoPlayCards(seat),
                seat.isRobot() ? RandCodeUtil.random(1, ROBOT_ACTION_MAX_SECONDS)
                    : (seat.sysHost ? HOST_ACTION_SECONDS : playTime),
                TimeUnit.SECONDS, gameExecutor);
  }

  /**
   * 自动出牌 .
   * 
   * @param seat 座位信息.
   */
  /*
   * private . void autoPlayCards(DouDiZhuAISeat seat) { DouDiZhuTable table = seat.table;
   * //上一次出牌也是我，即是第一手出牌 . if (table.prePlayOrder == seat.order) { List<DouDiZhuCard> minCards =
   * CardsTypeFinder.findMinCards(seat.cards); doPlayCards(seat, minCards,
   * CardsTypeGetter.getCardsType(minCards)); } else { if (seat.robot) {// 机器人出牌 List<DouDiZhuCard>
   * biggerCards . = CardsTypeFinder.findBiggerCards(seat.cards, table.prePlayCards,
   * table.prePlayCardsType); //找不到比上家更大的牌，放弃出牌 if (biggerCards == null) { doAbandonPlayCards(seat);
   * } else { doPlayCards(seat, biggerCards, CardsTypeGetter.getCardsType(biggerCards)); } } else {
   * doAbandonPlayCards(seat); } } }
   */
  private void autoPlayCards(DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;
    // 上一次出牌也是我，即是第一手出牌
    if (table.prePlayOrder == seat.getOrder()) {

      if (seat.isRobot()) {
        List<DouDiZhuCard> playCards = seat.firstPlayCards();
        doPlayCards(seat, playCards, CardsTypeGetter.getCardsType(playCards));
      } else {
        List<DouDiZhuCard> playCards = CardsTypeFinder.findMinCards(seat.cards);
        doPlayCards(seat, playCards, CardsTypeGetter.getCardsType(playCards));
      }
    } else {
      if (seat.isRobot()) { // 机器人出牌
        List<DouDiZhuCard> playCards = seat.playCards();
        // 找不到比上家更大的牌，放弃出牌
        if (playCards == null) {
          doAbandonPlayCards(seat);
        } else {
          doPlayCards(seat, playCards, CardsTypeGetter.getCardsType(playCards));
        }
      } else {
        doAbandonPlayCards(seat);
      }
    }
  }

  /**
   * 玩家出牌 .
   * 
   * @param player 玩家.
   * @param cardIds .
   */
  public void playCards(Player player, List<Integer> cardIds) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[斗地主]玩家[{}][{}]未进入牌桌，不能出牌");
      return;
    }

    DouDiZhuTable table = seat.table;
    if (seat.getOrder() != table.nextPlayOrder) {
      LOG.error("[斗地主]现在不该玩家[{}][{}]出牌", player.getPlayerName(), player.getId());
      return;
    }

    if (cardIds.size() == 0) {
      LOG.error("[斗地主]玩家[{}][{}]出的牌不能为空", player.getPlayerName(), player.getId());
      return;
    }

    if (cardIds.size() > 20) {
      LOG.error("[斗地主]玩家[{}][{}]出牌数量超过20张", player.getPlayerName(), player.getId());
      return;
    }

    // 将玩家出的牌按照数字排序
    TreeMultimap<Integer, DouDiZhuCard> sortedCards = TreeMultimap.create();
    for (Integer cardId : cardIds) {
      if (cardId < 0 || cardId >= DouDiZhuCard.values().length) {
        LOG.error("[斗地主]玩家[{}][{}]出的牌[{}]不存在", player.getPlayerName(), player.getId(), cardId);
        return;
      } else {
        DouDiZhuCard card = DouDiZhuCard.card(cardId);
        if (!seat.cards.contains(card)) {
          LOG.error("[斗地主]玩家[{}][{}]手上没有改牌[{}]", player.getPlayerName(), player.getId(), cardId);
          return;
        }
        sortedCards.put(card.num, card);
      }
    }

    // 玩家所出的牌(已经按照牌的大小从小到大排列过)
    List<DouDiZhuCard> playSortedCards = new ArrayList<>(sortedCards.values());
    // 当前玩家的出牌类型
    DouDiZhuCardsType playCardsType = CardsTypeGetter.getCardsType(playSortedCards);
    if (playCardsType == null) {
      LOG.error("[斗地主]玩家[{}][{}]出的牌[{}]不符合规则", player.getPlayerName(), player.getId(),
          playSortedCards);
      return;
    }

    // 上一次玩家出的牌
    List<DouDiZhuCard> prePlayCards = table.prePlayCards;
    // 上一次玩家出的牌的类型
    DouDiZhuCardsType preCardsType = table.prePlayCardsType;

    // 玩家首个出牌
    if (prePlayCards == null) {
      doPlayCards(seat, playSortedCards, playCardsType);
      return;
    }

    // 玩家出牌后别人不要
    if (prePlayCards != null && table.prePlayOrder == table.nextPlayOrder) {
      doPlayCards(seat, playSortedCards, playCardsType);
      return;
    }

    // 牌的比较结果 (-2:出牌数量或牌型不匹配,-1：我小，0：相等，1：我大)
    int compareRes =
        CardsTypeComparator.compare(playSortedCards, playCardsType, prePlayCards, preCardsType);

    // 我的牌如果不是王炸或者炸弹那么数量必须和上一家出牌数量一致
    if (compareRes == -2) {
      LOG.error("[斗地主]玩家[{}][{}]出牌[{}]数量或牌型和上家[{}]不匹配", player.getPlayerName(), player.getId(),
          playSortedCards, prePlayCards);
      return;
    }

    // 我的牌没有上家大
    if (compareRes == -1 || compareRes == 0) {
      LOG.error("[斗地主]玩家[{}][{}]出牌[{}]没有上家[{}]大", player.getPlayerName(), player.getId(),
          playSortedCards, prePlayCards);
      return;
    }

    doPlayCards(seat, playSortedCards, playCardsType);
  }

  /**
   * 玩家出牌 .
   * 
   * @param seat 座位信息.
   * @param playCards .
   * @param playCardsType .
   */
  private void doPlayCards(DouDiZhuAiSeat seat, List<DouDiZhuCard> playCards,
      DouDiZhuCardsType playCardsType) {
    DouDiZhuTable table = seat.table;
    table.prePlayOrder = seat.getOrder();
    table.prePlayCards = playCards;
    table.prePlayCardsType = playCardsType;
    table.nextPlayOrder = table.nextSeatOrder(seat);
    seat.playNum++;
    seat.prePromptCards = null;
    seat.prePromptCardsType = null;
    seat.cards.removeAll(playCards);
    seat.stepFuture.cancel(false);

    // 重新计算牌型
    seat.calcCardsType();
    if (playCardsType == DouDiZhuCardsType.ZHA_DAN || playCardsType == DouDiZhuCardsType.WANG_ZHA) {
      // 倍数 * 2 .
      table.multiple <<= 1;
    }

    msgMgr.sendPlayCardsMsg(table, seat, playCards, playCardsType);
    LOG.info("[斗地主]玩家[{}][{}]出牌[{}]", seat.getPlayerName(), seat.getPlayerId(),
        playCards.toString());

    if (seat.cards.size() == 0) {
      gameOver(table, seat);
    } else {
      schedulePlayCards(table.nextSeat(seat));
    }
  }

  /**
   * 玩家放弃出牌 .
   * 
   * @param player 玩家.
   */
  public void abandonPlayCards(Player player) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[斗地主]玩家[{}][{}]未进入牌桌，不能出牌", player.getPlayerName(), player.getId());
      return;
    }

    DouDiZhuTable table = seat.table;
    if (seat.getOrder() != table.nextPlayOrder) {
      LOG.error("[斗地主]现在不该玩家[{}][{}]出牌不能放弃出牌", player.getPlayerName(), player.getId());
      return;
    }

    if (table.prePlayOrder == table.nextPlayOrder) {
      LOG.error("[斗地主]玩家[{}][{}]头家出牌不能放弃", player.getPlayerName(), player.getId());
      return;
    }

    doAbandonPlayCards(seat);
  }

  /**
   * 放弃出牌 .
   * 
   * @param seat 座位信息.
   */
  private void doAbandonPlayCards(DouDiZhuAiSeat seat) {
    seat.prePromptCards = null;
    seat.prePromptCardsType = null;
    seat.stepFuture.cancel(false);
    DouDiZhuTable table = seat.table;
    DouDiZhuAiSeat nextSeat = table.nextSeat(seat);
    table.nextPlayOrder = nextSeat.getOrder();
    // 发送放弃出牌消息
    msgMgr.sendAbandonMsg(table, seat.getOrder());
    LOG.info("[斗地主]玩家[{}][{}]放弃出牌", seat.getPlayerName(), seat.getPlayerId());
    schedulePlayCards(nextSeat);
  }

  /**
   * 当前牌桌游戏结束,结算 .
   * 
   * @param table .
   * @param winner . 赢家 .
   */
  private void gameOver(DouDiZhuTable table, DouDiZhuAiSeat winner) {
    DouDiZhuAiSeat landlord = table.landlord();
    DouDiZhuAiSeat farmer1 = table.nextSeat(landlord);
    DouDiZhuAiSeat farmer2 = table.preSeat(landlord);
    boolean landlordWin = winner.landlord;

    /*
     * 农民积分变更=游戏底数*游戏倍数*农民倍数（选择加倍为2，不加倍为1）*地主倍数（选择加倍为2，不加倍为1）*输赢系数（输为-1，赢为1）。 .
     * 地主积分变更=（农民1积分变更+农民2积分变更）*-1。 . 游戏中出现炸弹、火箭、春天、反春天时，每出现一次，房间倍数*2。 春天：地主出牌后，在其他玩家未出牌时，地主将手牌出完。
     * 反春天：地主只出过一手牌，任一农民（允许相互配合）将手牌出光。 .
     */
    if (landlordWin && farmer1.cards.size() == FARMER_CARDS
        && farmer2.cards.size() == FARMER_CARDS) { // 春天
      // 倍数 * 2 .
      table.multiple <<= 1;
    } else if (!landlordWin && landlord.playNum == 1) { // 反春天
      // 倍数 * 2 .
      table.multiple <<= 1;
    }

    int tableCedits = table.baseCedits * table.multiple * (landlord.doubled ? 2 : 1);
    int farmer1Cedits = tableCedits * (farmer1.doubled ? 2 : 1) * (landlordWin ? -1 : 1);
    int farmer2Cedits = tableCedits * (farmer2.doubled ? 2 : 1) * (landlordWin ? -1 : 1);
    int landlordCedits = -(farmer1Cedits + farmer2Cedits);

    landlord.cedits += landlordCedits;
    farmer1.cedits += farmer1Cedits;
    farmer2.cedits += farmer2Cedits;

    LOG.info("[斗地主]玩家[{}][{}]出完所有牌,[{}]获胜,地主[{}][{}]积分[{}],农民[{}][{}]积分[{}],农民[{}][{}]积分[{}]",
        winner.getPlayerName(), winner.getPlayerId(), winner.landlord ? "地主" : "农民",
        landlord.getPlayerName(), landlord.getPlayerId(), landlordCedits, farmer1.getPlayerName(),
        farmer1.getPlayerId(), farmer1Cedits, farmer2.getPlayerName(), farmer2.getPlayerId(),
        farmer2Cedits);

    msgMgr.sendGameOverMsg(table, landlord, landlordCedits, farmer1, farmer1Cedits, farmer2,
        farmer2Cedits);

    for (DouDiZhuAiSeat s : table.seats) {
      s.reset();
    }

    // 牌桌游戏结束处理
    afterGameOver(table);
    table.reset();

    // 广播比赛中的座位信息变化
    for (DouDiZhuTable tbl : table.match.tables) {
      msgMgr.sendTableSeatInfosUpdateMsg(tbl);
    }
  }

  /**
   * 牌桌游戏结束处理,主要处理比赛相关的东西 .
   * 
   * @param table .
   */
  private void afterGameOver(DouDiZhuTable table) {
    DouDiZhuMatch match = table.match;
    List<DouDiZhuAiSeat> tableSeats = table.seats;
    // 比赛中的seats
    List<DouDiZhuAiSeat> matchSeats = match.seats;
    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper =
        matchRoomDic.get(match.room.getId().intValue());

    // 底分
    int baseCedits = table.baseCedits;
    // 预赛截止人数
    int preEndNum = matchRoomDomWrapper.getPreEndNum();

    /*
     * 模拟空桌子的机器人积分变化(30%的概率1-16倍正负积分) .
     */
    for (DouDiZhuAiSeat seat : matchSeats) {
      if (seat.isRobot() && seat.table == null) {
        long cedits = RandCodeUtil.random(1, 16) * baseCedits;
        seat.cedits = RandCodeUtil.probable(30, 100) ? -cedits : cedits;
      }
    }

    // 对正在比赛的座位排序
    sortMatchSeats(matchSeats);

    if (match.runs == 0) { // 预赛
      /*
       * 预赛淘汰比赛中的负分玩家,淘汰后的人数不能少于预赛截止人数(复制matchSeats循环) .
       */
      // eliminateSeat会从桌子中删除桌位，先把座位中的player记录下来
      Collection<Long> players = table.players();
      for (DouDiZhuAiSeat seat : new ArrayList<>(matchSeats)) {
        if (seat.cedits < 0 && matchSeats.size() > preEndNum) {
          if (seat.table == table) { // 在桌子中实际比赛的seat被淘汰才通知
            msgMgr.sendPreEliminationMsg(seat, players);
          }
          eliminateSeat(match, seat);
        }
      }

      // 对正在比赛的座位排序
      sortMatchSeats(matchSeats);

      if (matchSeats.size() > preEndNum) { // 预赛淘汰比赛中的负分玩家后预赛未截止

        if (tableSeats.size() < 3) { // 本桌有人被淘汰
          if (table.hasRealPlayer()) {
            if (assigneSeats(table)) {
              scheduleReady(table);
            } else {
              // 分桌失败，本桌预赛其实已经结束
              table.preliminaryOver = true;
              // 分配桌子失败，客户端等待
              msgMgr.sendAssigneSeatsFailMsg(table);
            }
          } else {
            // 该桌没有真实玩家解散
            disbandTable(table);
            // 分桌失败，本桌预赛其实已经结束
            table.preliminaryOver = true;
          }
        } else {
          scheduleReady(table);
        }
      } else if (matchSeats.size() == preEndNum) { // 预赛截止
        table.preliminaryOver = true;
        msgMgr.sendMatchRunsOverMsg(table);
        // 预赛是否结束
        boolean preliminaryOver = true;
        for (DouDiZhuTable tbl : match.tables) {
          if (!tbl.preliminaryOver) {
            preliminaryOver = false;
            break;
          }
        }

        msgMgr.sendMatchRunsUpdateMsg(match);
        if (preliminaryOver) {
          matchRunsOver(match);
        }
      }
    } else {
      /*
       * 决赛，所有真实玩家打满指定局数结束，开始下一轮，否则重新开始比赛 .
       */
      table.completedFinalsRuns++;
      int runsTimes = matchRoomDomWrapper.getFinalsRunsTimes();
      // 比赛决赛是否结束(所有真实玩家打满指定轮数就算)
      boolean finalsOver = true;
      // 桌子决赛是否结束(所有真实玩家打满指定轮数就算)
      boolean tableFinalsOver = table.completedFinalsRuns == runsTimes;

      for (DouDiZhuTable tbl : match.tables) {
        if (tbl.completedFinalsRuns < runsTimes) {
          finalsOver = false;
          break;
        }
      }

      if (tableFinalsOver) {
        msgMgr.sendMatchRunsOverMsg(table);
      } else {
        // schedule准备
        scheduleReady(table);
      }

      msgMgr.sendMatchRunsUpdateMsg(match);
      if (finalsOver) {
        matchRunsOver(match);
      }
    }
  }

  /**
   * 解散桌子 .
   * 
   * @param table .
   */
  private void disbandTable(DouDiZhuTable table) {
    for (DouDiZhuAiSeat seat : table.seats) {
      seat.reset();
      seat.table = null;
    }

    table.seats.clear();
  }

  /**
   * 淘汰座位 .
   * 
   * @param seat 座位信息.
   */
  private void eliminateSeat(DouDiZhuMatch match, DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;
    match.seats.remove(seat);
    match.eliminatedSeats.add(seat);

    if (table != null) {
      msgMgr.sendTableSeatInfosUpdateMsg(table);
      table.seats.remove(seat);
    }

    if (!seat.isRobot()) {
      giveRankRewark(seat);
      // 淘汰后schedule退赛
      seat.stepFuture = scheduleMgr.schedule(() -> {
        doWithdraw(seat);
        Player player = playerMgr.getPlayer(seat.getPlayerId());
        if (player != null) {
          exitRoom(player);
        }
      }, 20, TimeUnit.SECONDS, gameExecutor);
    }
  }

  /**
   * 发放比赛排名奖励 .
   * 
   * @param seat 座位信息.
   */
  private void giveRankRewark(DouDiZhuAiSeat seat) {
    DouDiZhuMatch match = seat.table.match;
    DouDiZhuRoom room = match.room;
    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper =
        matchRoomDic.get(match.room.getId().intValue());
    List<Reward> rewards = matchRoomDomWrapper.getRewards(seat.rank);

    if (rewards != null && rewards.size() > 0) {
      for (Reward reward : rewards) {
        backpackMgr.addItem(seat.getPlayerId(), reward.getId(), reward.getNum(),
            LogReason.DOUDIZHU_GIVE_RANK_REWARK);
      }
    }

    msgMgr.sendRankRewardInfoMsg(seat, rewards);

    LOG.info("[斗地主]玩家 [{}][{}]参与房间[{}][{}]斗地主比赛[{}]获得奖励[{}]", seat.getPlayerId(),
        seat.getPlayerName(), room.getId(), room.getName(), match.id, JSON.toJSONString(rewards));
    DbLogService.log(new DouDiZhuMatchLog(playerMgr.selectPlayer(seat.getPlayerId()), match,
        seat.rank, JSON.toJSONString(rewards)));
  }

  /**
   * 分配桌子座位,剩余玩家不足3个或者没有剩余真实玩家 .
   * 
   * @param table .
   * @return true:分配成功,false:分配失败 .
   */
  private boolean assigneSeats(DouDiZhuTable table) {
    DouDiZhuMatch match = table.match;
    List<DouDiZhuAiSeat> seats = match.seats;
    // 临时可分配桌子seats
    List<DouDiZhuAiSeat> tmpSeats = new ArrayList<>();
    tmpSeats.addAll(table.seats);

    for (DouDiZhuTable t : match.tables) {
      if (t != table && t.seats.size() < SEAT_NUM) {
        tmpSeats.addAll(t.seats);
      }
    }

    ArrayList<DouDiZhuAiSeat> disorderSeats = new ArrayList<>(seats);
    Collections.shuffle(disorderSeats);
    for (DouDiZhuAiSeat seat : disorderSeats) {
      if (seat.table == null) {
        tmpSeats.add(seat);
      }
    }

    if (tmpSeats.size() < SEAT_NUM) {
      return false;
    }

    List<DouDiZhuAiSeat> assigneSeats = tmpSeats.subList(0, SEAT_NUM);
    for (int order = 0; order < SEAT_NUM; order++) {
      DouDiZhuAiSeat seat = assigneSeats.get(order);
      seat.table = table;
      seat.setOrder(order);
    }

    table.seats.clear();
    table.seats.addAll(assigneSeats);

    return true;
  }

  /**
   * 比赛轮数结束 .
   * 
   * @param match .
   */
  private void matchRunsOver(DouDiZhuMatch match) {
    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper =
        matchRoomDic.get(match.room.getId().intValue());
    // 下一路
    int nextRuns = match.runs + 1;
    // 下一轮人数
    int nextAdvanceNum = 0;
    if (nextRuns <= matchRoomDomWrapper.getFinalsRuns()) {
      nextAdvanceNum = matchRoomDomWrapper.getFinalsAdvanceNum(nextRuns);
    }

    List<DouDiZhuAiSeat> seats = match.seats;
    // 对正在比赛的座位排序
    sortMatchSeats(seats);
    List<DouDiZhuAiSeat> seatsCopy = new ArrayList<>(seats);
    for (int i = nextAdvanceNum; i < seatsCopy.size(); i++) {
      eliminateSeat(match, seatsCopy.get(i));
    }
    // 对正在比赛的座位排序
    sortMatchSeats(seats);

    if (nextRuns <= matchRoomDomWrapper.getFinalsRuns()) {
      startMatchRuns(match, nextRuns);
    }
  }

  /**
   * 开始比赛 .
   * 
   * @param table .
   */
  private void startGame(DouDiZhuTable table) {
    DouDiZhuMatch match = table.match;
    DoudizhuMatchRoomDomWrapper matchRoomDomWrapper =
        matchRoomDic.get(match.room.getId().intValue());
    // 基础底分
    int baseCedits = 0;
    if (match.runs == 0) { // 预赛，基础底分 + 间隔时间 * 翻倍值 .
      int interval = (int) ((System.currentTimeMillis() - match.matchTime)
          / (1000 * matchRoomDomWrapper.getPreChangeTime()));
      baseCedits =
          matchRoomDomWrapper.getPreCedits() + interval * matchRoomDomWrapper.getPreChangeCedits();
    } else { // 决赛,底分固定
      baseCedits = matchRoomDomWrapper.getFinalsCedits();
    }
    table.baseCedits = baseCedits;
    msgMgr.sendGameStartMsg(table);
    doDealCards(table);
  }

  /**
   * 排名正在比赛的座位 .
   * 
   * @param seats .
   */
  private void sortMatchSeats(List<DouDiZhuAiSeat> seats) {
    seats.sort((s1, s2) -> {
      if (s1.cedits > s2.cedits) {
        return -1;
      } else if (s1.cedits < s2.cedits) {
        return 1;
      } else {
        if (s1.applyOrder > s2.applyOrder) {
          return 1;
        } else if (s1.applyOrder < s2.applyOrder) {
          return -1;
        } else {
          return 0;
        }
      }
    });

    // 重新
    for (int i = 0; i < seats.size(); i++) {
      seats.get(i).rank = i + 1;
    }
  }

  /**
   * 玩家提示出牌 .
   * 
   * @param player 玩家.
   */
  public void prompt(Player player) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[斗地主]玩家[{}][{}]未进入牌桌，不能提示");
      return;
    }

    DouDiZhuTable table = seat.table;
    if (seat.getOrder() != table.nextPlayOrder) {
      LOG.error("[斗地主]现在不该玩家[{}][{}]出牌，不能提示", player.getPlayerName(), player.getId());
      return;
    }
    if (table.prePlayOrder == seat.getOrder()) { // 玩家第一手出牌
      LOG.error("[斗地主]玩家[{}][{}]第一手出牌，不能提示", player.getPlayerName(), player.getId());
      return;
    }
    // 本次提示应该出的牌
    List<DouDiZhuCard> promptCards = null;
    // 本次提示应该出的牌类
    DouDiZhuCardsType promptCardsType = null;

    if (seat.prePromptCards == null) {
      promptCards =
          CardsTypeFinder.findBiggerCards(seat.cards, table.prePlayCards, table.prePlayCardsType);
      if (promptCards == null) {
        doAbandonPlayCards(seat);
        return;
      }
    } else {
      promptCards =
          CardsTypeFinder.findBiggerCards(seat.cards, seat.prePromptCards, seat.prePromptCardsType);
      if (promptCards == null) {
        promptCards =
            CardsTypeFinder.findBiggerCards(seat.cards, table.prePlayCards, table.prePlayCardsType);
      }
    }

    if (promptCards != null) {
      promptCardsType = CardsTypeGetter.getCardsType(promptCards);
      msgMgr.sendPromptMsg(player, promptCards);
    }
    seat.prePromptCards = promptCards;
    seat.prePromptCardsType = promptCardsType;
  }

  /**
   * 玩家主动退赛，在游戏中玩家会被托管 .
   * 
   * @param player 玩家.
   */
  public void withdraw(Player player) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat != null) {
      doWithdraw(seat);
    }

    // 玩家是否在房间中
    if (player.curRoom instanceof DouDiZhuRoom) {
      // 退出房间
      exitRoom(player);
    }
  }

  /**
   * 玩家退出房间 .
   * 
   * @param player 玩家.
   */
  public void exitRoom(Player player) {
    AbstractRoom room = player.curRoom;
    if (room instanceof DouDiZhuRoom) {
      player.curRoom = null;
      room.getPlayers().remove(player.getId());
      // 触发退出房间事件
      eventMgr.post(new ExitRoomEvent(player, room));
    } else {
      LOG.warn("[斗地主]玩家[{}][{}]不在斗地主房间中，无法退出房间", player.getPlayerName(), player.getId());
    }
  }

  /**
   * 退赛 .
   * 
   * @param seat 座位信息.
   */
  private void doWithdraw(DouDiZhuAiSeat seat) {
    DouDiZhuTable table = seat.table;
    DouDiZhuMatch match = table.match;
    if (match.eliminatedSeats.contains(seat)) { // 已经被淘汰了
      dataMgr.removePlayerSeat(seat.getPlayerId());
      seat.stepFuture.cancel(false);
    } else {
      seat.sysHost = true;
      msgMgr.sendTableSeatInfosUpdateMsg(table);
    }

    Player player = playerMgr.getPlayer(seat.getPlayerId());
    if (player != null && player.curRoom instanceof DouDiZhuRoom) {
      // 避免托管后玩家进入其他游戏，如果玩家在其他游戏发送退赛会退出桌子
      msgMgr.sendWithdrawMsg(seat);
    }
  }

  /**
   * 玩家喊话 .
   * 
   * @param player 玩家.
   * @param type . 1:嘲讽,2:催牌,3:赞扬 .
   */
  public void shout(Player player, int type) {
    DouDiZhuAiSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null || type < 1 || type > 3) {
      return;
    }

    msgMgr.sendShoutMs(seat, type);
  }
}
